<!--
  - 滚球算法 Rolling-Ball Algorithm Baseline Correction
 -->
 
<!-- 视图层 --><!-- 页头(上) -->
<template><header-comp />

<!-- 功能简介 -->
<uni-card title="功能简介">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "To新手：
    　　读取数据后，直接去最下面点“绘图预览”。
    　　再根据谱图情况调参。"
  />

  <!-- 文字 -->
  <view><text>
    　　滚球算法，Rolling-Ball Algorithm，也叫滚珠算法。基线校正算法，其主要处理步骤为：\n
    　　① “压平”。设定一个[滚球半径r]。遍历所有信号点，以每个信号点x的前后一个[滚球半径r]（即[x - r, x + r]）为窗口，找到窗口内Y信号点的[最小值]。遍历完毕，即得到一组[最小值]组成的Y。\n
    　　② “抬升”。遍历所有[最小值]Y，继续以[x - r, x + r]为窗口，找到窗口内[最小值]Y中的[最大值]。遍历完毕，即得到一组[(最小值的)最大值]组成的Y。\n
    　　③ “平滑”(可选)。设定一个[平滑半径R]。以[x - R, x + R]为窗口，找到窗口内[最小值]和[(最小值的)最大值]的平均值。遍历完毕，即得到一组[平均值]组成的Y。\n
    　　②或③得到的Y即为基线。将基线从原数据中扣除，即可得到校正后的数据。\n
    　　原理：\n
    　　当1/2峰宽小于[滚球半径r]时，“压平”会抹除该峰；“抬升”会将基线尽可能抬高至接近真实基线。\n
    　　当1/2峰宽大于[滚球半径r]时，“压平”会抹该峰1/2峰宽小于[滚球半径r]位置及以上的峰；“抬升”会将该处基线尽可能抬高至未被抹平的峰高度处。\n
    　　该算法就如同一个球在波谱数据的下方滚动，峰宽小于球直径的，球照常滚动不受影响；峰宽大于球的，球就落进大峰能容纳球直径的高度，然后滚出来，继续滚动。形成一条球滚动的线即为基线。因此叫做“滚球算法”。
  </text></view>

  <!-- 二级标题：参考文献 -->
  <uni-section
    title="参考文献"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容：文献 -->
    <view><text>
      　　文献链接：
    </text></view>
    <!-- 链接：文献 -->
    <uni-link
      text="https://doi.org/10.1016/0168-583X(95)00908-6"
      href="https://doi.org/10.1016/0168-583X(95)00908-6"
      class="center" color="blue"
    />
    <!-- 文字内容：库 -->
    <view><text>
      　　库链接：
    </text></view>
    <!-- 链接：库 -->
    <uni-link
      text="https://www.npmjs.com/package/ml-rolling-ball-baseline"
      href="https://www.npmjs.com/package/ml-rolling-ball-baseline"
      class="center" color="blue"
    />
  </uni-section>

</uni-card>

<!-- 读取数据 -->
<uni-card title="读取数据">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "打开含有波谱数据集的Excel文件。"
  />

  <!-- 二级标题：快速开始 -->
  <uni-section
    v-if="!isGetXlsxFile"
    title="快速开始"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　一键读取示例数据，快速体验功能。
    </text></view>
    <!-- 读取示例数据按钮 -->
    <view class="center">
      <button
        @click="readExampleData()"
        size="mini" type="primary"
      >
        快速读取示例数据
      </button>
    </view>
  </uni-section>

  <!-- 二级标题：下载示例数据文件 -->
  <uni-section
    title="示例数据下载"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　操作步骤：\n
      　　① 下载示例Excel模板文件。\n
      　　② 根据模板格式，将你自己的数据拷贝至模板。\n
      　　③ 保存模板文件，然后直接点“点击按钮读取数据文件”读取你的数据文件即可。
    </text></view>
    <!-- 示例文件下载按钮 -->
    <view class="center">
      <button
        @click="downloadExampleFile()"
        size="mini" type="primary"
      >
        示例Excel文件下载
      </button>
    </view>
  </uni-section>

  <!-- 二级标题：读取数据文件 -->
  <uni-section
    title="读取数据"
    type="circle"
    padding="0 1em"
  >
    <!-- 文件上传容器 -->
    <uni-file-picker
      title="　　点击按钮读取数据文件："
      file-extname="xlsx, xls" fileMediatype="all"
      limit="1" return-type="object" :del-icon="false"
      :auto-upload="false" :disable-preview="true"
      @select="(event) => readDataFile(event)"
    />
  </uni-section>

  <!-- 二级标题：数据形状 -->
  <!-- 条件渲染：得到数据才能渲染 -->
  <uni-section
    v-if="isGetXlsxFile"
    title="数据形状"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　需确定是“正置谱图”还是“倒置谱图”。\n
      　　紫外光谱为典型的正置谱图；红外光谱为典型的倒置谱图。\n
      　　默认“正置”。
    </text></view>
    <!-- 数据形状 -->
    <view class="center">
      倒置　<switch
        :checked="isMaxCriteria"
        @change="(event) => { isMaxCriteria = event.detail.value }"
        type="switch"
      />　正置
    </view>
  </uni-section>
  
</uni-card>

<!-- 数据预处理。条件渲染：有数据才渲染 -->
<data-prep-comp
  v-if="isGetXlsxFile"
  :isMinToZero="dataPrepProps.isMinToZero"
  :isMinZoom="dataPrepProps.isMinZoom"
  :isMaxToOne="dataPrepProps.isMaxToOne"
  :changeValueFunc="dataPrepProps.changeValueFunc"
/>

<!-- 折叠面板。条件渲染：有数据才渲染 -->
<uni-collapse v-if="isGetXlsxFile">

<!-- 滚球算法调参 -->
<uni-card title="滚球算法调参">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "滚球算法的参数微调，有滚球半径r、平滑半径R。"
  />
  <!-- 下拉框容器 -->
  <uni-collapse-item title="下拉查看设置详情">

    <!-- 二级标题：调参界面 -->
    <uni-section
      title="调参界面"
      type="circle"
      padding="0 1em"
    >
      <!-- 文字内容 -->
      <view><text>
        　　滚球算法可设置的参数有:\n
        　　滚球半径r。即“滚球”的观察窗的半径。默认“50”。\n
        　　平滑半径R。即平滑基线操作的观察窗半径。默认“0”，即不平滑。
      </text></view>
      <!-- 输入框的表格容器 -->
      <uni-table
        :border="true" :stripe="true"
      >
        <!-- 标题行 -->
        <uni-tr>
          <uni-th align="center">
            键
          </uni-th>
          <uni-th align="center">
            值
          </uni-th>
        </uni-tr>
        <!-- 内容行：滚球半径r -->
        <uni-tr>
          <uni-td align="center">
            滚球半径r
          </uni-td>
          <uni-td>
            <uni-easyinput
              v-model="windowM"
              type="number"
              placeholder="请输入滚球半径r"
              trim="all" :clearable="false"
            />
          </uni-td>
        </uni-tr>
        <!-- 内容行：平滑半径R -->
        <uni-tr>
          <uni-td align="center">
            平滑半径R
          </uni-td>
          <uni-td>
            <uni-easyinput
              v-model="windowS"
              type="number"
              placeholder="请输入平滑半径R"
              trim="all" :clearable="false"
            />
          </uni-td>
        </uni-tr>
      </uni-table>
    </uni-section>

  </uni-collapse-item>
</uni-card>

<!-- 结果栏 -->
<uni-card title="结果栏">
  <!-- 通告栏 -->
  <uni-notice-bar text=
    "预览操作的结果，并导出结果。
    可反复修改参数预览，最后导出自己满意的结果。"
  />

  <!-- 二级标题：结果预览 -->
  <uni-section
    title="结果预览"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　用第一组波谱数据进行试处理，并对处理结果进行绘图预览。\n
    </text></view>
    <!-- 绘图预览相关按钮容器 -->
    <view class="center">
      <!-- 绘图预览按钮 -->
      <button
        @click="previewResult()"
        size="mini" type="primary"
      >
        绘图预览
      </button>
      <!-- 打开图表板按钮 -->
      <button
        @click="showVisor()"
        size="mini" type="primary"
      >
        打开图表板
      </button>
    </view>
  </uni-section>

  <!-- 二级标题：数据批量处理 -->
  <uni-section
    title="数据批量处理"
    type="circle"
    padding="0 1em"
  >
    <!-- 文字内容 -->
    <view><text>
      　　根据各项设置，批量处理全部波谱数据，并将数据结果导出为Excel文件。\n
      　　所导出的文件格式与输入文件格式一致，分为“去基线”、“基线”2个表。
    </text></view>
    <!-- 执行操作按钮 -->
    <view class="center">
      <button
        @click="resultToDownload()"
        size="mini" type="primary"
      >
        一键导出
      </button>
    </view>
  </uni-section>

</uni-card>

</uni-collapse>

<!-- 页脚(下) -->
<footer-comp /></template>

<!-- 逻辑层 -->
<script setup>

// 导入vue的onMounted、ref方法
import { shallowRef } from "vue"
// 导入数据预处理组件
import dataPrepComp from "../index/data-prep-comp.vue"
// 封装一个数据预处理组件的传参对象
const dataPrepProps = {
  isMinToZero: false,
  isMinZoom: false,
  isMaxToOne: false,
  changeValueFunc: (key, value) => {
    dataPrepProps[key] = value
  }
}
// 导入tfjs-visor库
// import * as tf from "@tensorflow/tfjs"
// import * as tfvis from "@tensorflow/tfjs-vis"
// 导入xlsx库
import * as XLSX from "xlsx"
// 导入ml库的滚球算法
import { rollingBall } from "ml-rolling-ball-baseline"
// 导入公共方法库的方法
import {
  // 下载文件
  downloadFile,
  // 数组转置
  arrayTranspose,
  // 打开图表栏
  showVisor,
  // 下载示例文件
  downloadExampleFile,
  // 读取XLSX文件为工作簿对象
  readXlsxFile,
  // 从工作表提取原始数据
  sheetToDataArr,
  // 用tfvis渲染Aoa数据为图表
  aoaTotfvisChart,
  // 数据归一化
  minToZero, maxToOne
} from "../../scripts/app_common.js"
// 导入示例数据集
import { exampleData } from "../../datasets/example-data.js"

/**
 * @ref对象
 */
// 是否读取到数据的boolean对象
const isGetXlsxFile = shallowRef(null)
// 谱图正置或倒置的boolean对象
const isMaxCriteria = shallowRef(true)
// 滚球算法的2个number对象
const windowM = shallowRef(50)
const windowS = shallowRef(0)

/**
 * @其他对象
 */
// 原始数据整理后得到的数据数组
let dataJsonArr = null

/**
 * @readDataFile 读取数据文件
 * @function async
 * @param { Event } event 点击事件
 * 读取文件并转置，然后调用文件数据检查修复方法solveRawData()
 * 成功后绘图，并修改isGetXlsxFile状态
 */
async function readDataFile(event) { try {
  // 显示加载圈
  uni.showLoading({ title: "正在读取文件数据" })
  // 先清零dataJsonArr数据
  dataJsonArr = null
  // 从event中获取工作簿对象
  const workbook = await readXlsxFile(event.tempFiles[0].file)
  // 将工作簿第1张表转为Json化的数据数组
  dataJsonArr = sheetToDataArr(workbook.Sheets[workbook.SheetNames[0]])
  // 如果数据检查成功通过，接到返回数据
  if (dataJsonArr) {
    // 拿第一个数据作图
    aoaTotfvisChart(
      // 图表类型
      "scatterplot",
      // 图表容器
      { tab: "滚球算法", name: "第一组数据" },
      // 图表数据：第一个数据
      [dataJsonArr[0]],
      // 数据序列标签
      ["原始数据"]
    )
    // 操作结束，isGetXlsxFile的ref对象标记为true
    isGetXlsxFile.value = true
  }
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("readDataFile()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "读取数据文件报错",
    content: error,
  })
}}

/**
 * @readExampleData 直接读取示例数据
 * @function
 */
function readExampleData() { try {
  // 把示例数据组装成AOA形式，直接赋值给dataJsonArr即可
  dataJsonArr = [exampleData]
  // 拿第一个数据作图
  aoaTotfvisChart(
    // 图表类型
    "scatterplot",
    // 图表容器
    { tab: "滚球算法", name: "示例数据" },
    // 图表数据：第一个数据
    [dataJsonArr[0]],
    // 数据序列标签
    ["原始数据"]
  )
  // 打开图表栏
  showVisor()
  // 修改isGetXlsxFile状态
  isGetXlsxFile.value = true
} catch (error) {
  console.error("readExampleData()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "读取示例数据报错",
    content: error,
  })
}}

/**
 * @dataProcess 数据处理，相当于本业务内部的公共方法
 * 会读取页面ref对象的参数，对数据进行处理
 * @function
 * @param {{
 *   x: Number[],
 *   y: Number[],
 *   header: String | Number
 * }} dataJson 单组数据对象
 * @returns {{
 *   yDeepCopy: Number[],
 *   yBaseline: Number[],
 *   yCorrected: Number[]
 }} 返回的结果，含基线、校正、迭代次数、误差
 */
function dataProcess(dataJson) { try {
  // 深拷贝dataJson对象的y
  const yDeepCopy = JSON.parse(JSON.stringify(dataJson.y))
  // 预处理：零位校正
  if (dataPrepProps.isMinToZero) {
    minToZero(yDeepCopy, dataPrepProps.isMinZoom, isMaxCriteria.value)
  }
  // 预处理：上限归一
  if (dataPrepProps.isMaxToOne) {
    maxToOne(yDeepCopy, isMaxCriteria.value)
  }
  // 如果是倒置峰，要反向处理一下
  if (!isMaxCriteria.value) {
    for (let i = 0; i < yDeepCopy.length; i++) {
      yDeepCopy[i] = -yDeepCopy[i]
    }
  }
  // 滚球处理
  const yRollingBall = rollingBall(
    // 谱图数据
    yDeepCopy,
    {
      // 滚球半径，需得是整数
      windowM: Math.round(windowM.value),
      // 平滑半径，需得是整数
      windowS: Math.round(windowS.value)
    }
  )
  // 得到校正值
  const yCorrected = []
  for (let i = 0; i < yDeepCopy.length; i++) {
    yCorrected.push(yDeepCopy[i] - yRollingBall[i])
  }
  // 如果是倒置峰，要反向处理一下
  if (!isMaxCriteria.value) {
    for (let i = 0; i < yCorrected.length; i++) {
      yDeepCopy[i] = -yDeepCopy[i]
      yRollingBall[i] = -yRollingBall[i]
      yCorrected[i] = -yCorrected[i]
    }
  }
  // 处理完毕，返回处理结果
  return {
    yDeepCopy: yDeepCopy,
    yBaseline: yRollingBall,
    yCorrected: yCorrected
  }
} catch (error) {
  console.error("dataProcess()报错: ", error)
  uni.showModal({
    showCancel: false,
    title: "数据运算报错",
    content: error,
  })
}}

/**
 * @previewResult 绘图预览
 * @function
 * @note 直接或间接读取页面ref参数，串联所有操作方法
 */
function previewResult() { try {
  // 显示加载圈
  uni.showLoading({ title: "正在处理数据，请稍候..." })
  // 用第一条数据简单处理一下
  const rollingBallResult = dataProcess(dataJsonArr[0])
  // 确定成功接到处理结果数据
  if (!rollingBallResult) {
    // 没接收到？那就退出
    // 隐藏加载圈
    uni.hideLoading()
    return
  }
  // 绘图预览
  aoaTotfvisChart(
    // 图表类型
    "scatterplot",
    // 图表容器
    { tab: "滚球算法", name: "数据预览(第1组)" },
    // 图表数据
    [
      { x: dataJsonArr[0].x, y: rollingBallResult.yDeepCopy },
      { x: dataJsonArr[0].x, y: rollingBallResult.yBaseline },
      { x: dataJsonArr[0].x, y: rollingBallResult.yCorrected },
    ],
    // 数据序列标签
    ["处理前", "基线", "校正"]
  )
  // 最后，打开图表版呈现数据
  showVisor()
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("previewResult()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "结果预览报错",
    content: error,
  })
}}

/**
 * @resultToDownload 干到结果并下载
 * 执行处理操作，结果作为Excel表格下载
 * @function
 * @note 直接或间接读取页面ref参数，串联所有操作方法
 */
function resultToDownload() { try {
  // 显示加载圈
  uni.showLoading({ title: "正在处理数据，请稍候..." })
  // 建个接数据的数组
  const baselineAoa = []
  const correctedAoa = []
  for (let i = 0; i < dataJsonArr.length; i++) {
    // 处理数据，生成结果
    const rollingBallResult = dataProcess(dataJsonArr[i])
    // 得判断成功搞到数据了
    if (!rollingBallResult) {
      // 没搞到数据？直接跳出
      uni.hideLoading()
      return
    }
    // 得判断，是第一个值，或x和前面的不一样
    if ((i === 0) || (dataJsonArr[i].x !== dataJsonArr[i - 1].x)) {
      // 基线，需要额外怼一列X轴数据
      baselineAoa.push(["X"].concat(Array.from(dataJsonArr[i].x)))
      baselineAoa.push([dataJsonArr[i].header].concat(Array.from(rollingBallResult.yBaseline)))
      // 校正，需要额外怼一列X轴数据
      correctedAoa.push(["X"].concat(Array.from(dataJsonArr[i].x)))
      correctedAoa.push([dataJsonArr[i].header].concat(Array.from(rollingBallResult.yCorrected)))
    // 如果不是第一个值，而且x和前面一样
    } else {
      // 基线，不用X轴数据了，直接Y
      baselineAoa.push([dataJsonArr[i].header].concat(Array.from(rollingBallResult.yBaseline)))
      // 校正，不用X轴数据了，直接Y
      correctedAoa.push([dataJsonArr[i].header].concat(Array.from(rollingBallResult.yCorrected)))
    }
  }
  // 生成表格（先转置）
  // aoa_to_sheet()方法接受Array-of-Array对象，FLoat64Array不行
  // 如遇FLoat64Array，一定要提前用Array.from()转成普通Array
  const sheetBaselineAoa = XLSX.utils.aoa_to_sheet(arrayTranspose(baselineAoa))
  const sheetCorrectedAoa = XLSX.utils.aoa_to_sheet(arrayTranspose(correctedAoa))
  // 创建一个新的工作簿对象
  const workbook = XLSX.utils.book_new()
  // 将工作表添加到工作簿，该表名为"波谱数据集"
  XLSX.utils.book_append_sheet(workbook, sheetCorrectedAoa, "波谱数据集_去基线")
  XLSX.utils.book_append_sheet(workbook, sheetBaselineAoa, "波谱数据集_基线")
  // 以工作簿生成ArrayBuffer对象
  const workbookArrBufObj = XLSX.write(workbook, { type: "buffer" })
  // 以xlsx格式下载该工作簿ArrayBuffer对象
  downloadFile(workbookArrBufObj, "波谱数据集_去基线.xlsx")
  // 隐藏加载圈
  uni.hideLoading()
} catch (error) {
  console.error("resultToDownload()报错: ", error)
  uni.hideLoading()
  uni.showModal({
    showCancel: false,
    title: "数据导出报错",
    content: error,
  })
}}


</script>
